/*
     Copyright 2007 Joseph M. Ferner

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections.Generic;
using log4net;
using OpenServers.Net.Common.FileSystem;
using OpenServers.Net.Common.System;
using OpenServers.Net.Common.Utils;
using OpenServers.Net.HttpServer.FileSystem;
using OpenServers.Net.HttpServer.Request;
using OpenServers.Net.HttpServer.State;

namespace OpenServers.Net.HttpServer {
    /// <summary>
    /// processes the http request.
    /// </summary>
    public class HttpProcessor : IHttpProcessor {
        private static readonly ISystem _system = new DefaultSystem();
        private static readonly ILog log = LogManager.GetLogger(typeof(HttpProcessor));
        private readonly IHttpServer _server;

        /// <summary>
        /// constructor.
        /// </summary>
        public HttpProcessor(IHttpServer server) {
            _server = server;
        }

        /// <summary>
        /// called when data is received from the client and the current state is header.
        /// </summary>
        public void DataReceived(HttpState state, byte[] buffer, int bytesRead) {
            for (int i = 0; i < bytesRead; i++) {
                char ch = (char)buffer[i];
                if (ch != '\r') {
                    if (ch == '\n') {
                        if (state.RequestBuffer.Length != 0) { // according to RFC-1945 some requests will be prepended with new lines. se we need to skip them.
                            state.RequestBuffer.Append(ch);
                            if (state.RequestBuffer.ToString(state.RequestBuffer.Length - 2, 2) == "\n\n") {
                                string headerStr = state.RequestBuffer.ToString();
                                state.RequestBuffer.Remove(0, state.RequestBuffer.Length);
                                ParseHeaders(state, headerStr);
                                state.RequestState = HttpState.RequestStateEnum.Body;
                            }
                        }
                    } else {
                        state.RequestBuffer.Append(ch);
                    }
                }
            }
        }

        /// <summary>
        /// called when data is received from the client and the current state is body.
        /// </summary>
        public void ProcessBody(HttpState state, byte[] buffer, int bytesRead) {
            // TODO: implement body processing, only when the request is a POST.
            state.RequestState = HttpState.RequestStateEnum.BodyComplete;
        }

        /// <summary>
        /// Parses the HTTP headers and populates the HttpState object.
        /// </summary>
        protected virtual void ParseHeaders(HttpState state, string headerString) {
            string[] headerLines = headerString.Split('\n');
            ParseRequestLine(state, headerLines[0]);
            string lastHeaderName = null;
            if (state.Response.ResponseCode == HttpResponseCode.SC200) {
                for (int i = 1; i < headerLines.Length - 2; i++) { // skip the last 2. These are the end of header markers
                    lastHeaderName = ParseHeaderLine(state, headerLines[i], lastHeaderName);
                }
            }
        }

        /// <summary>
        /// Sends the response to the client.
        /// </summary>
        public void SendResponse(HttpState state) {
            if (!state.Response.Headers.ContainsKey("Date")) {
                string dateString = DateUtils.ToHttpString(_system.DateTimeNow);
                state.Response.Headers.Add("Date", dateString);
            }
            if (!state.Response.Headers.ContainsKey("Server")) {
                state.Response.Headers.Add("Server", _server.ServerName);
            }
            if (!state.Response.Headers.ContainsKey("Content-Type")) {
                state.Response.Headers.Add("Content-Type", "text/html");
            }

            if (state.Response.ResponseCode != HttpResponseCode.SC200) {
                SendErrorResponse(state);
            } else {
                IFile file = ResolveUrl(state);
                if (file == null || !file.Exists) {
                    // TODO show directory listing?
                    state.Response.ResponseCode = HttpResponseCode.SC404;
                    SendErrorResponse(state);
                } else {
                    if (_server.SecurityProvider.CanRead(state.User, file)) {
                        if (file is IHttpFile) {
                            IHttpFile httpFile = (IHttpFile)file;
                            state.Response.ContentType = httpFile.ContentType;
                        }
                        state.Response.Length = file.Size;
                        state.Response.DataStream = file.OpenInputStream();
                        state.Listener.Send(state, state.Response.GetStream());
                    } else {
                        state.Response.ResponseCode = HttpResponseCode.SC401;
                        SendErrorResponse(state);
                    }
                }
            }

            // TODO: check the keep alive status.
            state.RequestState = HttpState.RequestStateEnum.Header;
        }

        private IFile ResolveUrl(HttpState state) {
            IFileSystemObject fso = _server.FileSystem.GetObject(state.Request.Url.Path);

            if (fso != null && fso.Exists) {
                if (fso is IDirectory) {
                    IList<IFileSystemObject> dirItems = ((IDirectory)fso).GetItems();
                    foreach (string defaultPage in _server.DefaultPages) {
                        foreach (IFileSystemObject dirItem in dirItems) {
                            if (dirItem is IFile && dirItem.Name == defaultPage) {
                                return (IFile)dirItem;
                            }
                        }
                    }
                    return null;
                } else if (fso is IFile) {
                    return (IFile)fso;
                }
            }
            return null;
        }

        private void SendErrorResponse(HttpState state) {
            // TODO: write send response code.
            throw new NotImplementedException();
        }

        /// <summary>
        /// Parses the first line of the HTTP header.
        /// </summary>
        protected virtual void ParseRequestLine(HttpState state, string line) {
            int firstSpace = line.IndexOf(' ');
            int lastSpace = line.LastIndexOf(' ');
            if (firstSpace > 0) {
                string requestVerb = line.Substring(0, firstSpace);
                string requestUrl;
                double requestVersion;
                string versionString;
                if (lastSpace != firstSpace) {
                    versionString = line.Substring(lastSpace + 1);
                    requestUrl = line.Substring(firstSpace + 1, lastSpace - firstSpace - 1);
                } else {
                    versionString = "HTTP/1.0";
                    requestUrl = line.Substring(firstSpace + 1);
                }
                if (versionString == "HTTP/1.0") {
                    requestVersion = 1.0;
                } else if (versionString == "HTTP/1.1") {
                    requestVersion = 1.1;
                } else {
                    requestVersion = 1.1;
                }
                state.Request = new HttpRequest(requestVerb, new Url(requestUrl), requestVersion);
            } else {
                state.Response.ResponseCode = HttpResponseCode.SC400;
            }
        }

        /// <summary>
        /// Parses HTTP header lines, other than the first line.
        /// </summary>
        protected virtual string ParseHeaderLine(HttpState state, string line, string lastHeaderName) {
            if (line.StartsWith(" ") || line.StartsWith("\t")) {
                if (lastHeaderName == null) {
                    throw new ApplicationException(string.Format("invalid header line [{0}]", line));
                }
                line = line.TrimStart();
                state.Request.Headers[lastHeaderName] += line;
                return lastHeaderName;
            } else {
                int nameValueSplit = line.IndexOf(':');
                if (nameValueSplit > 0) {
                    string name = line.Substring(0, nameValueSplit).Trim();
                    string value = line.Substring(nameValueSplit + 1).Trim();
                    return AddHeaderNameValue(state, name, value);
                } else {
                    throw new ApplicationException(string.Format("invalid header line [{0}]", line));
                }
            }
        }

        /// <summary>
        /// adds a header name value pair to the state object.
        /// </summary>
        protected virtual string AddHeaderNameValue(HttpState state, string name, string value) {
            if (state.Request.Headers.ContainsKey(name)) {
                string newValue = state.Request.Headers[name];
                newValue += ", " + value;
                state.Request.Headers[name] = newValue;
            } else {
                state.Request.Headers.Add(name, value);
            }
            return name;
        }
    }
}
